/*
 * The MIT License
 *
 * Copyright 2012 http://generic-toolset.googlecode.com.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.rlpw.swing;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.event.EventListenerList;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;

/**
 *
 * @param <T> the type of the elements
 */
public class JOCheckTree<T> extends JOTree<T> implements Checkable<T> {

    private CheckModel<T> checked;
    private EventListenerList listeners = new EventListenerList();
    private CheckTreeRenderer checkRenderer;
    private boolean recursiveCheckable = true;

    /**
     * Constructs a empty <code>JOCheckTree</code> which displays the default root node.
     * @param c the type of displayed items which is used for the reflection
     */
    public JOCheckTree(Class<T> c) {
        super(c);
        initCheckSupport();
    }

    /**
     * Constructs a <code>JOCheckTree</code> which displays the default root node.
     * The tree is created using the specified data model.
     * @param c the type of displayed items which is used for the reflection
     * @param model the data model of displayed items
     */
    public JOCheckTree(Class<T> c, Model<T> model) {
        super(c, model);
        initCheckSupport();
    }

    /**
     * Constructs a <code>JOCheckTree</code> which displays the specified root node.
     * The tree is created using the specified data model.
     * @param c the type of displayed items which is used for the reflection
     * @param model the data model of displayed items
     * @param root displays the specified root node
     */
    public JOCheckTree(Class<T> c, Model<T> model, Object root) {
        super(c, model, root);
        initCheckSupport();
    }

    private void initCheckSupport() {
        checked = new DefaultCheckModel(this);
        checkRenderer = new CheckTreeRenderer();
        super.setCellRenderer(checkRenderer);

    }

    /** {@inheritDoc} */
    @Override
    public CheckModel<T> getCheckModel() {
        return checked;
    }

    /** {@inheritDoc} */
    @Override
    public void setCellRenderer(TreeCellRenderer cellRenderer) {
        if (checkRenderer != null) {
            checkRenderer.setRenderer(cellRenderer);
        } else {
            super.setCellRenderer(cellRenderer);
        }
    }

    /**
     * Returns {@code true} if recursive selection is enabled, else {@code false}. 
     * @return {@code true} if the specified index is checked,
     *         else {@code false}
     */
    public boolean isRecursiveCheckable() {
        return recursiveCheckable;
    }

    /**
     * Turns on or off the ability for recursive selection.
     * The default value of this property equals {@code true}.
     * 
     * @param recursiveCheckable whether or not to enable recursive selection
     */
    public void setRecursiveCheckable(boolean recursiveCheckable) {
        this.recursiveCheckable = recursiveCheckable;
        updateView();
    }

    /**
     * Adds an {@code ActionListener} to the {@code JOCheckTree}.
     * 
     * @param l the {@code ActionListener} to be added
     */
    public void addActionListener(ActionListener l) {
        listeners.add(ActionListener.class, l);
    }

    /**
     * Removes an {@code ActionListener} from the {@code JOCheckTree}.
     * 
     * @param l the {@code ActionListener} to be removed
     */
    public void removeActionListener(ActionListener l) {
        listeners.remove(ActionListener.class, l);
    }

    /**
     * Notifies all listeners that have registered interest for notification on this event type.<br/>
     * Updated all added views on the current {@code Model}. 
     * 
     * @see  org.rlpw.swing.Model#updateViews
     */
    protected synchronized void fireActionEvent() {
        getDataModel().updateViews();
        ActionEvent event = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "");
        for (ActionListener l : listeners.getListeners(ActionListener.class)) {
            l.actionPerformed(event);
        }
    }

    private class CheckTreeRenderer implements TreeCellRenderer {

        private TreeCellRenderer renderer;
        private boolean isRootSelected = false;

        public CheckTreeRenderer() {

            renderer = new DefaultTreeCellRenderer();

            addMouseListener(new MouseAdapter() {

                @Override
                public void mouseClicked(MouseEvent e) {

                    TreePath path = getPathForLocation(e.getX(), e.getY());
                    if (path == null) {
                        return;
                    }
                    Rectangle rect = getRowBounds(getRowForLocation(e.getX(), e.getY()));
                    if (e.getX() < rect.getX() || e.getX() > 20 + rect.getX()) {
                        return;
                    }
                    Object item = path.getLastPathComponent();

                    boolean selected = false;

                    if (!item.equals(getRoot())) {
                        getCheckModel().setChecked((T) item, selected = !getCheckModel().isChecked((T) item));
                    } else {
                        selected = isRootSelected = !isRootSelected;
                    }
                    if (isRecursiveCheckable() && !getModel().isLeaf(item)) {
                        selectRecursive(item, selected);
                    }
                    fireActionEvent();
                }
            });
        }

        private void selectRecursive(Object parent, boolean selected) {
            for (int i = 0; i < getModel().getChildCount(parent); i++) {
                Object child = getModel().getChild(parent, i);
                getCheckModel().setChecked((T) child, selected);
                if (!getModel().isLeaf(child)) {
                    selectRecursive(child, selected);
                }
            }
        }

        @Override
        public Component getTreeCellRendererComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
            Component comp = renderer.getTreeCellRendererComponent(tree, value, isSelected, expanded, leaf, row, hasFocus);
            if (!isRecursiveCheckable() && value.equals(getRoot())) {
                return comp;
            }

            JCheckBox box = new JCheckBox();
            box.setEnabled(tree.isEnabled());
            box.setSelected(value.equals(getRoot()) ? isRootSelected : getCheckModel().isChecked((T) value));


            JPanel panel = new JPanel(new BorderLayout());
            panel.add(box, BorderLayout.WEST);
            panel.add(comp, BorderLayout.CENTER);
            adapt(tree, isSelected, box);
            adapt(tree, isSelected, panel);
            return panel;
        }

        private void adapt(JTree tree, boolean isSelected, JComponent comp) {
            comp.setOpaque(false);
            comp.setForeground(tree.getForeground());
            comp.setBackground(tree.getBackground());
        }

        TreeCellRenderer getRenderer() {
            return renderer;
        }

        void setRenderer(TreeCellRenderer renderer) {
            this.renderer = renderer;
        }
    }
}
